package org.bhf.validation;

/**
 * Copyright (c) 2007, Yahoo! Inc. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;
import org.bhf.validation.annotations.Validator;
import org.bhf.validation.annotations.ValidatorRegistry;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * This class generates validators for arrays, collections, and pojos. Each validator looks something like...
 * <pre>
 * public final class someGeneratedClassName
 * {
 *     public static void validate( DeclaredType pojo, Validator validator )
 *         throws IOException
 *     {
 *         ...
 *     }
 * }
 * </pre>
 */
final class ValidatorGenerator
{
    public static final int DEPTH_LIMIT = 25;

    /**
     * Generate a vadliator for the given type.
     *
     * @param type The type of object to be validated
     * @return Class implementing the validator for this type
     * @throws java.io.IOException Write error creating class
     */
    Class<?>    generateFor( final Type type )
        throws IOException
    {
        final StringBuffer body = new StringBuffer();

        generateForComponent( type, body, "pojo", 0 );

        return generateWriterFromMethodBody( type, body.toString() );
    }

    //
    // Private
    //

    //
    // POJOs
    //

    /**
     * Generate code for writing a POJO/JavaBean (property list)
     *
     * @param pojo Class of POJO
     * @param body Code accumulator
     * @param accessor Expression used to access the element: array index, etc.
     * @param depth Nesting level
     * @throws IOException Write error
     */
    private void        generateForPOJO( final Class<?> pojo, final StringBuffer body, final String accessor, final int depth )
        throws IOException
    {
        if( depth > ValidatorGenerator.DEPTH_LIMIT )
            throw new CyclicGraphException();

        // Properties
        final Method[] methods = pojo.getMethods();

        body.append( "{" );
        for( final Method method : methods )
        {
            if( Util.isGetter( method ) )
            {
                final Annotation[]      annotations     = method.getAnnotations();
                final String            nestedAccessor  = accessor + "." + method.getName() + "()";
                final Type              valueType       = method.getGenericReturnType();
                final String            wrappedAccessor;
                final List<Annotation>  validators      = new ArrayList<Annotation>(8);

                if( valueType instanceof Class && Class.class.cast( valueType ).isPrimitive() )
                {
                    final Class<?> wrapperClass = getWrapper( valueType );
                    wrappedAccessor = "new " + wrapperClass.getName() + "(" + nestedAccessor + ")";
                }
                else
                {
                    wrappedAccessor = nestedAccessor;
                }

                for( final Annotation annotation : annotations )
                {
                    if( Validator.Util.isValidator( annotation ) )
                    {
                        validators.add( annotation );
                    }
                }

                if( validators.size() > 0 )
                {
                    boolean needsDelimiter = false;

                    body.append( "stack.add(\"" ); body.append( Util.methodNameToProperty( method ) ); body.append( "\");" );
                    body.append( "if(" );

                    for( final Annotation validator : validators )
                    {
                        final String key = ValidatorRegistry.register( validator, method );

                        if( needsDelimiter )
                            body.append( "&&" );
                        body.append( validator.annotationType().getName() );
                        body.append( ".Validator.validate(" );
                            body.append( wrappedAccessor );
                            body.append( ",\"" ); body.append( key );
                            body.append( "\",messages,stack)" );
                        needsDelimiter = true;
                    }

                    body.append( "){" );

                    // Dig deeper...
                    if( !endOfTheRoad( valueType ) )
                    {
                        generateForComponent( valueType, body, nestedAccessor, depth + 1 );
                    }
                    body.append( "}" );
                    body.append( "stack.remove( stack.size() - 1 );" );
                }
            }
        }
        body.append( "}" );
    }

    //
    // Collections
    //

    /**
     * Generate code for writing a Map
     *
     * @param map Type of map
     * @param body Code accumulator
     * @param accessor Expression used to access the element: array index, etc.
     * @param depth Nesting level
     * @throws IOException Write error
     */
    private void generateForMap( final Type map, final StringBuffer body, final String accessor, final int depth )
        throws IOException
    {
        final Type[]    componentTypess     = Util.getMapComponentTypes( map );
        final Type      keyType             = componentTypess[0];
        final Type      valueType           = componentTypess[1];
        final String    castKey             = Util.getDeclarationName( keyType, true );
        final String    castValue           = Util.getDeclarationName( valueType, true );

        body.append( "if(" ).append( accessor ).append( "!=null){java.util.Iterator i=" ).append( accessor ).append( ".entrySet().iterator();while(i.hasNext()){" );
        body.append( "java.util.Map.Entry e=(java.util.Map.Entry)i.next();" );
        body.append( Util.getDeclarationName( keyType, true ) ).append( " k=(" ); body.append( castKey ).append( ")e.getKey();" );
        body.append( Util.getDeclarationName( valueType, true ) ).append( " v=(" ); body.append( castValue ).append( ")e.getValue();" );

        body.append( "stack.add(k.toString());" );
        if( valueType instanceof Class && Class.class.cast( valueType ).isPrimitive() )
        {
            generateForComponent( valueType, body, "v", depth + 1 );
        }
        else
        {
            body.append( "if(v!=null){" );
                generateForComponent( valueType, body, "v", depth + 1 );
            body.append( "}" );
        }
        body.append( "stack.remove( stack.size() - 1 );" );

        body.append( "}}" );
    }

    //
    // Collections
    //

    /**
     * Generate code for validating the elements a Collection
     *
     * @param collection Type of collection
     * @param body Code accumulator
     * @param accessor Expression used to access the element: array index, etc.
     * @param depth Nesting level
     * @throws IOException Write error
     */
    private void generateForCollection( final Type collection, final StringBuffer body, final String accessor, final int depth )
        throws IOException
    {
        final Type      componentType       = Util.getCollectionComponentType( collection );
        final String    cast                = Util.getDeclarationName( componentType, true );

        body.append( "if(" ).append( accessor ).append( "!=null){int l=" ).append( accessor ).append( ".size();for(int i=0;i<l;i++){" );
        body.append( Util.getDeclarationName( componentType, true ) ).append( " v=(" );
        body.append( cast );
        body.append( ")" );
        body.append( accessor );
        body.append( ".get(i);" );

        body.append( "stack.add(Integer.toString(i));" );
        if( componentType instanceof Class && Class.class.cast( componentType ).isPrimitive() )
        {
            generateForComponent( componentType, body, "v", depth + 1 );
        }
        else
        {
            body.append( "if(v!=null){" );
                generateForComponent( componentType, body, "v", depth + 1 );
            body.append( "}" );
        }
        body.append( "stack.remove( stack.size() - 1 );" );

        body.append( "}}" );
    }

    //
    // Arrays
    //

    /**
     * Generate array writing code based on Class
     *
     * @param array Class of array
     * @param body Code accumulator
     * @param accessor Expression used to access the element: array index, etc.
     * @param depth Nesting level
     * @throws IOException Write error
     */
    private void    generateForArray( final Class array, final StringBuffer body, final String accessor, final int depth )
        throws IOException
    {
        final Class     componentType   = array.getComponentType();
        final String    cast            = Util.getDeclarationName( componentType, true );

        body.append( "if(" );
        body.append( accessor );
        body.append( "!=null){int l=" );
        body.append( accessor );
        body.append( ".length;for(int i=0;i<l;i++){" );
        body.append( Util.getDeclarationName( componentType, true ) ); body.append( " v=(" ); body.append( cast );
        body.append( ")" );
        body.append( accessor );
        body.append( "[i];" );

        body.append( "stack.add(Integer.toString(i));" );
        if( componentType != null && Class.class.cast( componentType ).isPrimitive() )
        {
            generateForComponent( componentType, body, "v", depth + 1 );
        }
        else
        {
            body.append( "if(v!=null){" );
                generateForComponent( componentType, body, "v", depth + 1 );
            body.append( "}" );
        }
        body.append( "stack.remove( stack.size() - 1 );" );

        body.append( "}}" );
    }

    /**
     * Generate array writing code based on Type
     *
     * @param array Class of array
     * @param body Code accumulator
     * @param accessor Expression used to access the element: array index, etc.
     * @param depth Nesting level
     * @throws IOException Write error
     */
    private void    generateForArray( final GenericArrayType array, final StringBuffer body, final String accessor, final int depth )
        throws IOException
    {
        final Type      componentType   = array.getGenericComponentType();
        final String    cast            = Util.getDeclarationName( componentType, true );

        body.append( "if(" );
        body.append( accessor );
        body.append( "!=null){int l=" );
        body.append( accessor );
        body.append( ".length;for(int i=0;i<l;i++){" );
        body.append( Util.getDeclarationName( componentType, true ) ); body.append( " v=(" ); body.append( cast );
        body.append( ")" );
        body.append( accessor );
        body.append( "[i];" );

        body.append( "stack.add(Integer.toString(i));" );
        if( componentType instanceof Class && Class.class.cast( componentType ).isPrimitive() )
        {
            generateForComponent( componentType, body, "v", depth + 1 );
        }
        else
        {
            body.append( "if(v!=null){" );
                generateForComponent( componentType, body, "v", depth + 1 );
            body.append( "}" );
        }
        body.append( "stack.remove( stack.size() - 1 );" );

        body.append( "}}" );
    }

    /**
     * Generate inline code for a given component. This is where all the type switching occurs and the
     * messy mixing of Type and Class.
     *
     * @param componentType Type of component
     * @param body Code accumulator
     * @param indexer Expression used to access the element: array index, etc.
     * @param depth Nesting level
     * @throws IOException Write error
     */
    private void    generateForComponent( final Type componentType, final StringBuffer body, final String indexer, final int depth )
        throws IOException
    {
        if( componentType instanceof Class )
        {
            final Class clazz = Class.class.cast( componentType );

            if( clazz.isArray() )
            {
                generateForArray( clazz, body, indexer, depth + 1 );
            }
            else if( Map.class.isAssignableFrom( clazz ) )
            {
                generateForMap( clazz, body, indexer, depth + 1 );
            }
            else if( List.class.isAssignableFrom( clazz ) )
            {
                generateForCollection( clazz, body, indexer, depth + 1 );
            }
            else if( Object.class.equals( clazz ) ) // A bit bizarre. Code generator bug + erasure leaves us with just
                                                    // Object when we've lost type info. No choice but reflection.
            {
                body.append( "validator.validateObject(" ).append( indexer ).append( ");" );
            }
            else
            {
                generateForPOJO( clazz, body, indexer, depth + 1 );
            }
        }
        else if( componentType instanceof GenericArrayType )
        {
            generateForArray( GenericArrayType.class.cast( componentType ), body, indexer, depth + 1 );
        }
        else if( componentType instanceof ParameterizedType )
        {
            final ParameterizedType pType = ParameterizedType.class.cast( componentType );
            final Type rawType = pType.getRawType();

            if( rawType instanceof Class && Map.class.isAssignableFrom( (Class)rawType ) )
            {
                generateForMap( componentType, body, indexer, depth + 1 );
            }
            else if( rawType instanceof Class && List.class.isAssignableFrom( (Class)rawType ) )
            {
                generateForCollection( componentType, body, indexer, depth + 1 );
            }
            else if( rawType instanceof Class )
            {
                generateForPOJO( Class.class.cast( rawType ), body, indexer, depth + 1 );
            }
            else
            {
                body.append( "validator.validateObject(" ).append( indexer ).append( ");" );
            }
        }
        else // Object
        {
            throw new UnsupportedTypeException( componentType );
        }
    }

    //
    // Common
    //

    private boolean     endOfTheRoad( final Type clazz )
    {
        boolean toast = false;

        if( clazz instanceof Class )
        {
            final String n = (Class.class.cast(clazz)).getName();
            toast = n.startsWith( "java" );
        }

        return toast;
    }

    private Class<?>    getWrapper( final Type clazz )
    {
        if( clazz == Byte.TYPE )        return Byte.class;
        if( clazz == Short.TYPE )       return Short.class;
        if( clazz == Integer.TYPE )     return Integer.class;
        if( clazz == Long.TYPE )        return Long.class;
        if( clazz == Float.TYPE )       return Float.class;
        if( clazz == Double.TYPE )      return Double.class;
        if( clazz == Character.TYPE )   return Character.class;
        if( clazz == Boolean.TYPE )     return Boolean.class;

        throw new RuntimeException( "Attempt to wrap a non-primitive type" ); // Code bug
    }

    /**
     * Generate a writer, given the body of the write method.
     *
     * @param type The type of object to be written with this writer
     * @param methodBody Will be modified, do not reuse.
     * @return Class implementing the writer for this type
     * @throws IOException Write error
     */
    private Class<?>    generateWriterFromMethodBody( final Type type, final String methodBody )
        throws IOException
    {
        final ClassPool pool = Util.getClassPool();
        // Open class
        final CtClass validator = pool.makeClass( Util.getValidatorClassNameForType( type ) );

        // Open method
        final StringBuffer body = new StringBuffer();

        // validate( ... pojo, Validator validator)
        body.append( "public static void validate(" ).
            append( Util.getDeclarationName( type, true ) ).
            append( " pojo,org.bhf.validation.Validator validator) throws java.io.IOException {" );
        body.append( "org.bhf.i18n.Messages messages=validator.getMessages();" );
        body.append( "java.util.List stack=validator.getContext();" );
        body.append( methodBody );
        body.append( "}" );
        try
        {
//            System.out.println( body.toString() );
            // Close method
            {
                final CtMethod write = CtNewMethod.make( body.toString(), validator );
                validator.addMethod( write );
            }

            // Close class
            return validator.toClass();
        }
        catch( CannotCompileException e )
        {
            throw new RuntimeException( e ); // Soften because a compile error implies a problem with our generated code
        }
    }
}
